﻿using DataAccessObjectLayer;
using ModelLayer;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ControllerLayer
{
    public class ReservationCtr : IReservationCtr
    {
        private readonly Reservation _reservation;

        public ReservationCtr()
        {
            _reservation = new Reservation
            {
                Date = new DateTime(),
                Customer = null,
                Id = -1,
                PartReservations = new List<PartReservation>(),
            };
        }

        public ReservationCtr(Reservation resv)
        {
            _reservation = resv;
        }

        /// <summary>
        /// Sets a customer to the local reservation.
        /// </summary>
        /// <param name="customerId">The customer identifier.</param>
        public void SetCustomer(long customerId)
        {
            var personCtr = new PersonCtr();
            _reservation.Customer = personCtr.GetCustomer(customerId);
        }

        /// <summary>
        /// Gets the part reservations from a reservation in the database.
        /// </summary>
        /// <param name="resv">The resv.</param>
        /// <returns>List{PartReservation}.</returns>
        public List<PartReservation> GetPartReservations(Reservation resv)
        {
            var dbPartResv = new DBPartReservation();
            return dbPartResv.GetAllPartReservation(resv.Id);
        }

        /// <summary>
        /// Gets all reservations.
        /// </summary>
        /// <returns>List{Reservation}.</returns>
        public List<Reservation> GetAllReservations()
        {
            var dbResv = new DBReservation();
            return dbResv.GetAllReservation();
        }

        /// <summary>
        /// Gets all reservations from a customer in the database.
        /// </summary>
        /// <param name="customerId">The customer identifier.</param>
        /// <returns>List{Reservation}.</returns>
        public List<Reservation> GetAllReservationsFromCustomer(long customerId)
        {
            var dbResv = new DBReservation();
            return dbResv.GetAllReservationFromCustomer(customerId);
        }

        /// <summary>
        /// Creates a reservation using dijkstra's graph search algorithm to find a suitable route.
        /// </summary>
        /// <param name="startStation">The start station.</param>
        /// <param name="endStation">The end station.</param>
        /// <param name="cust">The customer.</param>
        /// <param name="date">The date.</param>
        /// <returns>Reservation.</returns>
        public Reservation CreateReservationUsingDijkstras(Station startStation, Station endStation, Customer cust, DateTime date)
        {
            var resv = new Reservation { Customer = cust, Date = date, Status = ReservationStatusEnum.Created };
            var routeCtr = new RouteCtr();
            var dbResv = new DBReservation();
            resv.Route = routeCtr.GenerateRouteUsingDijkstras(startStation, endStation);
            try
            {
                var traveledStations = TravelStations(resv.Route.PartRoutes, 160);
                var stationsWithReservedBat = ReserveBatteries(traveledStations, resv.Route.PartRoutes, 160);
                resv.PartReservations = GeneratePartReservations(stationsWithReservedBat, resv.Date);
                resv.Id = dbResv.InsertReservation(resv);
            }
            catch (Exception ex)
            {
                dbResv.RemoveReservation(resv.Id);
                routeCtr.RemoveRoute(resv.Route.Id);
                throw new Exception("Det var ikke muligt at booke en rute: " + ex.Message);
            }
            return resv;
        }

        public Reservation CreateReservationUsingAStar(Station startStation, Station endStation, Customer cust, DateTime date)
        {
            var resv = new Reservation { Customer = cust, Date = date, Status = ReservationStatusEnum.Created };
            var routeCtr = new RouteCtr();
            var dbResv = new DBReservation();
            resv.Route = routeCtr.GenerateRouteUsingAStar(startStation, endStation);
            try
            {
                var traveledStations = TravelStations(resv.Route.PartRoutes, 160);
                var stationsWithReservedBat = ReserveBatteries(traveledStations, resv.Route.PartRoutes, 160);
                resv.PartReservations = GeneratePartReservations(stationsWithReservedBat, resv.Date);
                resv.Id = dbResv.InsertReservation(resv);
            }
            catch (Exception ex)
            {
                dbResv.RemoveReservation(resv.Id);
                routeCtr.RemoveRoute(resv.Route.Id);
                throw new Exception("Det var ikke muligt at booke en rute: " + ex.Message);
            }
            return resv;
        }

        /// <summary>
        /// Reserves the batteries.
        /// </summary>
        /// <param name="stationList">The station list.</param>
        /// <param name="partRoutes">The part routes.</param>
        /// <param name="maxRange">The maximum range.</param>
        /// <returns>Dictionary{StationTimeSpan}.</returns>
        /// <exception cref="System.Exception">Der var ikke batterier på stationerne. or Der var
        /// ikke muligt at køre forbi stationen uden batterier. or Det var ikke muligt at booke en
        /// rute: + e.Message</exception>
        private Dictionary<Station, TimeSpan> ReserveBatteries(Dictionary<Station, TimeSpan> stationList, List<PartRoute> partRoutes, double maxRange)
        {
            var stationsWithBatteries = new Dictionary<Station, TimeSpan>(stationList);
            var stationCtr = new StationCtr();
            var tempStationList = new List<Station>();
            //Taking a list of stations that will be used to try making a battery reservation from
            foreach (var station in stationList)
            {
                tempStationList.Add(station.Key);
            }
            bool gotAllBatteries = stationCtr.RemoveBatteries(tempStationList);

            if (!gotAllBatteries)
            {
                var tmpStationList = new Dictionary<Station, TimeSpan>(stationList);
                try
                {
                    //try to remove a battery from each station
                    foreach (var s in stationList)
                    {
                        if (stationCtr.RemoveBattery(s.Key.Id))
                        {
                            tmpStationList.Remove(s.Key);
                        }
                        //if this cant be done, take a look on the previous and next station in the route
                        else
                        {
                            var previousStation = (from partRoute in partRoutes
                                                   where partRoute.LocationEnd == s.Key
                                                   select partRoute).FirstOrDefault() ?? new PartRoute();
                            var nextStation = (from partRoute in partRoutes
                                               where partRoute.LocationStart == s.Key
                                               select partRoute).FirstOrDefault() ?? new PartRoute();

                            if (maxRange > previousStation.Distance + nextStation.Distance && // distance over two
                                                                                              // partroutes less
                                                                                              // than max range?
                                previousStation.Distance + nextStation.Distance > 0 && //same distance bigger than 0 ?
                                !stationList.ContainsKey(previousStation.LocationStart) && //the list of station doesnt contain the previous station of the first station ?
                                !stationList.ContainsKey(nextStation.LocationEnd)) //the list of station doesnt contain the next station of the second station station ?
                            {
                                //Make a list with previous station of the first station and the next station of the second station
                                var newStations = new List<Station>
                                {
                                    previousStation.LocationStart,
                                    nextStation.LocationEnd
                                };
                                //..and try to remove a battery from these stations
                                if (stationCtr.RemoveBatteries(newStations))
                                {
                                    //If that was possible, add the stations to the list of stations with batteries
                                    stationsWithBatteries.Add(previousStation.LocationStart, previousStation.Time);
                                    stationsWithBatteries.Add(nextStation.LocationEnd, nextStation.Time);
                                }
                                else //batteries could not be removed from these stations
                                {
                                    throw new Exception("Der var ikke batterier på stationerne.");
                                }
                            }
                            else
                            {
                                throw new Exception("Der var ikke muligt at køre forbi stationen uden batterier.");
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    foreach (var station in stationList.Where(station => !tmpStationList.ContainsKey(station.Key)))
                    {
                        stationCtr.AddBattery(station.Key.Id);
                    }
                    throw new Exception("Det var ikke muligt at booke en rute: " + e.Message);
                }
            }
            return stationsWithBatteries;
        }

        /// <summary>
        /// Generates part reservations from a list of stations and a date.
        /// </summary>
        /// <param name="stationList">The station list.</param>
        /// <param name="date">The date.</param>
        /// <returns>List{PartReservation}.</returns>
        private List<PartReservation> GeneratePartReservations(Dictionary<Station, TimeSpan> stationList, DateTime date)
        {
            var partReservations = new List<PartReservation>();
            foreach (KeyValuePair<Station, TimeSpan> batteryReservation in stationList)
            {
                var pResv = new PartReservation
                {
                    Count = 1,
                    Station = batteryReservation.Key,
                    PickupDate = date.Add(batteryReservation.Value)
                };
                partReservations.Add(pResv);
            }
            return partReservations;
        }

        /// <summary>
        /// Updates the reservation.
        /// </summary>
        /// <param name="resv">The resv.</param>
        /// <returns>The number of affected rows</returns>
        public int UpdateReservation(Reservation resv)
        {
            resv.Status = ReservationStatusEnum.Accepted;
            var dbReservation = new DBReservation();
            return dbReservation.UpdateReservation(resv);
        }

        /// <summary>
        /// Removes the reservation.
        /// </summary>
        /// <param name="resv">The resv.</param>
        /// <returns>The number of affected rows</returns>
        public int CancelReservation(Reservation resv)
        {
            var dbReservation = new DBReservation();
            var stationCtr = new StationCtr();
            foreach (var pResv in resv.PartReservations)
            {
                stationCtr.AddBattery(pResv.Station.Id);
            }
            return dbReservation.RemoveReservation(resv.Id);
        }

        /// <summary>
        /// changes the status on a reservation in the database to accepted.
        /// </summary>
        /// <param name="resvId">The resv identifier.</param>
        /// <returns>The number of affected rows</returns>
        public int AcceptReservation(long resvId)
        {
            var resv = GetReservation(resvId);
            resv.Status = ReservationStatusEnum.Accepted;
            return UpdateReservation(resv);
        }

        /// <summary>
        /// Gets a reservation from the database.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns>Reservation.</returns>
        public Reservation GetReservation(long id)
        {
            var dbResv = new DBReservation();
            return dbResv.GetReservation(id);
        }

        /// <summary>
        /// Return a list of traveled stations.
        /// </summary>
        /// <param name="partRoutes">The part routes.</param>
        /// <param name="maxRange">The maximum range.</param>
        /// <returns>Dictionary{Station, TimeSpan}.</returns>
        private Dictionary<Station, TimeSpan> TravelStations(List<PartRoute> partRoutes, double maxRange)
        {
            var stationList = new Dictionary<Station, TimeSpan>();
            var currentRange = maxRange;
            var currentTimeSpent = new TimeSpan();
            PartRoute lastPartRoute = null;
            foreach (var partRoute in partRoutes)
            {
                //Add the current partroute's time to the currentTImeSpent
                currentTimeSpent = currentTimeSpent.Add(partRoute.Time);
                //Check to find out if the distance can be driven
                if (partRoute.Distance < currentRange)
                {
                    lastPartRoute = partRoute;
                    currentRange -= partRoute.Distance;
                }
                //If distance is to far to be driven with current fuel level
                else if (partRoute.Distance > currentRange)
                {
                    //If the distance of this partroute plus the distance of the earlier partroute is less than the maximum range
                    if (lastPartRoute != null && partRoute.Distance + lastPartRoute.Distance < maxRange)
                    {
                        // add the previous station to the stationlist (get a battery there)
                        stationList.Add(lastPartRoute.LocationEnd, currentTimeSpent);
                        //Update the current range to a full battery minus the distance from the last station
                        currentRange = maxRange - lastPartRoute.Distance;
                        lastPartRoute = partRoute;
                    }
                    else
                    {   //The distance over the two stations is too great, get a battery from each
                        if (lastPartRoute != null)
                        {
                            // add the previous station to the stationlist (get a battery there)
                            stationList.Add(lastPartRoute.LocationEnd, currentTimeSpent);
                        }
                        // add this station to the stationlist ( get a battery here)
                        stationList.Add(partRoute.LocationEnd, currentTimeSpent);
                        //Update the current range to a full battery
                        currentRange = maxRange;
                    }
                }
                else
                {
                    throw new Exception("Det var ikke muligt at køre ruten, afstand for stor.");
                }
            }
            var lastStation = partRoutes.Count;
            if (partRoutes.Count > lastStation)
                if (!stationList.ContainsKey(partRoutes[lastStation].LocationEnd))
                {
                    stationList.Add(partRoutes[lastStation].LocationEnd, partRoutes[lastStation].Time);
                }
            return stationList;
        }
    }
}